/**
 * 程序功能 
 *      当前破解进度 几位数，组合模式；
 *      停止后，记录上次状态，继续执行
 */
const fs = require('fs');
const path = require('path');
const unzipCMD = require('./script/unzipCMD');
const { exit } = require('process');
const numCPUs = require("os").cpus().length;

const getConfig = require("./script/config/config-mgr");
const Logger = require('./script/Logger');


let save_num_n = 0;
let del_all_n = 0;
// 密码生成配置相关
const nums = [1, 6]; // 密码可能位数 1~6
// const last_record = null;// 上次运行记录 null || {t: ,pwd:'xxx'}
let last_record = null;// 上次运行记录 null || {t: ,pwd:'xxx'}
// @todo 不同设备，获取cup数量后 ，多次测试计算出最优
const bfl = 16;  // 并发量?子进程数  numCPUs * 2
const is_show_dghs = false;// 显示单个平均耗时
const save_num = 100; // 每 save_num_n * bfl >= save_num 时存储，
/**@todo big file  */
const del_all_7z = 20;  // 每 del_all_n * bfl >= del_all_7z 时清空

const logger = new Logger({delay: {inter : 15*1000, n: 12}});



async function  main() {
    // for(let i = 0;  i< 100 ; i++){
        
    //     await  new  Promise((res,rej)=>{
    //         setTimeout(()=>{
    //             res();
    //         },20);
    //     }).then(_=>{
    //         logger.delay(`${i}`,`${i+2}`,`${i+3}`);
    //     });
    // }
    // setTimeout(()=>{},2000);
    // return;

    // param_parser();
    // return;
    // console.log("numCPUs: ", numCPUs);
    // load_data();
    // let xx = { "pwd": "8YA"};

    // save_data(JSON.stringify(xx));

    // 获取配置信息
    const config = getConfig();

    // var xpath = './extracted';//文件夹路径是否需要创建
    // xpath = path.join(xpath, '/xxx' );
    // deleteAllFileAndMe(xpath);

    // 暴破
    DVWA_Bingfa(config);
    /**@todo 文件存读当前破解进度 */
    /**@todo log4js使用 */

    // @todo密码生成 依赖包功能测试

    // @todo选择压缩文件功能

    // @todo多线程 计时功能
}

function param_parser() {
    let arr = process.argv;
    console.log(arr);
    if (arr.length == 3 && arr[2] == '-x') {
        // console.log("-x");
    }
}

function readFile(path, options) {
    return new Promise((res, rej) => {
        fs.readFile(path, options, (err, rv) => {
            if (err) {
                rej(err);
            } else {
                res(rv);
            }
        });
    });
}
function writeFile(path, str, options) {
    return new Promise((res, rej) => {
        fs.writeFile(path, str, options, (err, rv) => {
            if (err) {
                rej(err);
            } else {
                res(rv);
            }
        });
    });
}

/**
 * 加载上次存储数据
 * @param {string} k 
 */
async function load_data(k) {
    // console.log(k);
    const key = k;
    let data_path = path.join(__dirname, '/data/index.json');
    await readFile(data_path, 'utf8').then(res => {
        let all_data = JSON.parse(res);
        let data = all_data[key];
        if (data !== undefined) {
            last_record = {
                t: 1,
                pwd: data.pwd
            };
            console.log("load last: ", data.pwd);
        } else {
            // 初始化数据
            last_record = {
                t: 1,
                pwd: "0"
            };
            console.log("load last: ", "null");
        }
    }).catch(err => {
        console.log(err);
        throw new Error("读取数据文件失败!");
    });
    // console.log(11);

}
/**
 * @param {string} k 
 * @param {string} str JSON字符串
 */
async function save_data(k, str) {
    const key = k;
    let data_path = path.join(__dirname, '/data/index.json');
    let res1 = await readFile(data_path, 'utf8').catch(err => {
        console.log(err);
        throw new Error("读取数据文件失败!");
    });
    let all_data = JSON.parse(res1);
    let data = JSON.parse(str);
    all_data[key] = data;
    let str_ = JSON.stringify(all_data);
    // console.log(str_);

    await writeFile(data_path, str_, 'utf8').catch(err => {
        console.log(err);
        throw new Error("写入数据文件失败!");
    });
}

/**
 * 
 * @param {object} config 
 * @param {string} config.app 程序所调用的应用
 * @param {string} config.cmd 程序执行命令
 * @param {string} config.fileName 需要解压的文件名称
 */
async function DVWA_Bingfa({ app, cmd, fileName }) {
    // console.log(2);
    let finished = false;
    // return;

    var outPath = './extracted';//文件夹路径是否需要创建
    if (!fs.existsSync(outPath)) {
        fs.mkdirSync(outPath);
    }

    // let zipPath = './source/test1-0-123a.zip'; // zip经典加密
    // let zipPath = './source/test1-123a.zip';// zip非经典加密
    let zipPath = './source/' + fileName;// 
    const filename = path.parse(zipPath).name;
    // console.log("filename: ", filename);
    outPath = path.join(outPath, '/' + filename);
    

    if (!fs.existsSync(outPath)) {
        fs.mkdirSync(outPath); // 用压缩包名 创建压缩目录
    }else if(app === "7z"){
        deleteAllFileAndMe(outPath, false);
    }

    const outPath_7z =  outPath;


    await load_data(filename);



    zipPath = path.resolve(zipPath);
    outPath = path.resolve(outPath);
    // console.log("outPath: ", outPath);

    let getCmdStr = (function () {
        if (app === "WinRAR") {
            return function (pwd) {
                return cmd + ' -ibck -y x -t -p' + pwd + ' "' + zipPath + '"  "' + outPath + '"';
            }
        } else if (app === "7z") {
            return function (pwd) {
                /**
                 * 7z 和winRAR的程序执行逻辑有差异
                 * winRAR有密码的内容  命令行执行时携带密码，但密码错误时，文件不会解压出来
                 * 7z 有密码的内容  命令行执行时携带密码，但密码错误时，文件也会解压出来，文件格式错误
                 *      解决方法
                 *          @todo判断解压错误时的文件跟正常文件的区别。
                 *          再套个文件夹，错误的都删除。
                 */
                outPath = path.join(outPath_7z, '/' + pwd);

                if (fs.existsSync(outPath)) {
                    deleteAllFileAndMe(outPath,false);
                }else{
                    fs.mkdirSync(outPath); //  创建压缩目录
                }

                outPath = path.resolve(outPath);
                
            /**
             * -y 使 7-Zip 执行命令时的大多数提示失效。您可以使用此选项来阻止在 e (释放) 和 x (完整路径释放) 命令中文件覆盖时的提示。
                    7z x src.zip -y ：从 src.zip 释放所有文件。所有的覆盖提示将被阻止且所有相同文件名的文件将被覆盖。
             * -ao 指定在释放期间如何覆盖硬盘上现有的同名文件。
                    语法：-ao[a | s | u ]
                    -aoa 直接覆盖现有文件，而没有任何提示。 
                    -aos 跳过现有文件，其不会被覆盖。 
                    -aou 如果相同文件名的文件以存在，将自动重命名被释放的文件。举个例子，文件 file.txt 将被自动重命名为 file_1.txt。 
                    -aot 如果相同文件名的文件以存在，将自动重命名现有的文件。举个例子，文件 file.txt 将被自动重命名为 file_1.txt。 
                7z x test.zip -aoa ：从压缩档案 test.zip 中释放所有文件并却不做提示直接覆盖现有文件。
             */
                return cmd + ' x' + ' "' + zipPath + '" -aou -p' + pwd + ' -o"' + outPath + '"';
            }
        }
    }());

    let t = 1;
    let val = "0";
    if (!!last_record) {
        // 有上次的解压记录
        /** @todo 成功解压后，再次执行有问题吗？ */
        t = last_record.t;// @todo这个值没存储，有时间可以看要不要废弃了
        val = last_record.pwd;
    }
    let pwd = new CreatePwd({
        val,
        nums,
    });
    // console.time("xx");
    // let t_s = Date.now();

    // let t_s = 0;

    let t_s = Date.now();
    let isFoundPwd = false;
    let pwd_rel = '';
    while (!finished) {
        // console.log("times: ", t, " pwd: ", pwd.val); // 记录当前处理到的密码，方便中断重启
        // if (is_show_dghs) {
        //     t_s = Date.now();
        // }
        let pwd_s = pwd.val;
        logger.delay("并发批次: ", t, `开始 pwd: ${pwd_s}`);
        let arr = [];
        let data = [];

        save_num_n++;
        del_all_n++;

        let pwd_next = null;
        for (let i = 0; i < bfl; i++) {
            data.push({
                pwd: pwd.val,
            });
            let cmdStr = getCmdStr(pwd.val);
            arr.push(
                unzipCMD.unzipCmd(cmdStr)
            );

            let res = pwd.next();
            pwd_next = res.val;
            if (res == null) {
                console.log("密码穷解完了");
                finished = true;
                break;
            }
        }

        await Promise.allSettled(arr).then(res => {
            for (let index = 0; index < res.length; index++) {
                let el = res[index];
                let pwd = data[index].pwd;
                if (el.status === "fulfilled") {
                    let rv = el.value;
                    let code = rv;
                    if (code === 0) {

                        console.log("找到密码，密码为: ", pwd);
                        finished = true;
                        isFoundPwd = true;
                        pwd_rel = pwd;
                        break;
                    } else {
                        // console.log("密码错误");

                    }
                } else if (el.status === "rejected") {
                    let rv = el.reason;

                    if (app === "7z") {
                        // @todo 待优化逻辑 或者将app参数传到 unzipCMD.js中
                        /**
                         * 退出代码
                            0 ： 正常，没有错误；
                            1 ： 警告，没有致命的错误，例如某些文件正在被使用，没有被压缩；
                            2 ： 致命错误；
                            7 ： 命令行错误；
                            8 ： 没有足够的内存；
                            255 ： 用户停止了操作；
                         */
                        // console.log("密码错误");
                    } else {
                        console.log("程序出错 pwd: ", pwd);
                        // break;
                        exit(1);
                    }
                }
            };
        });
        if (is_show_dghs) {
            let t_e = Date.now();
            let time = t_e - t_s;
            let x = time / bfl;
            logger.delay("并发批次: ", t, `结束 pwd ${data[0].pwd} ~ ${data[bfl - 1].pwd}  平均单个耗时: ${x}`);
        } else {
            logger.delay("并发批次: ", t, `结束 pwd ${data[0].pwd} ~ ${data[bfl - 1].pwd}`);
        }

        /**@todo 这个值在特殊情况下有问题吗？ */
        if (save_num_n * bfl >= save_num || isFoundPwd) {
            if(isFoundPwd){
                pwd_next = pwd_rel;
            }
            let str_json = { "pwd": pwd_next };
            save_data(filename, JSON.stringify(str_json));
            save_num_n = 0;
            let t_e = Date.now();
            let diff = t_e - t_s;
            const h_ms = 60 * 60 * 1000;
            const m_ms = 60 * 1000;

            let h = parseInt(diff / h_ms);
            let m = parseInt(diff % h_ms / m_ms);
            let s = parseInt(diff % m_ms / 1000);
            logger.delay(`save last: ${pwd_next} gone: ${h} h : ${m} m : ${s} s  total: ${t * bfl}`);
        }

        if (del_all_n * bfl >= del_all_7z  && !isFoundPwd  && app === '7z') {
            del_all_n = 0;
            let out_path_root = path.resolve(outPath_7z);
            deleteAllFileAndMe(out_path_root,false);
            logger.delay(`clear dir...`);
        }

        /**
        // 测试pwd.next
        let res = pwd.next();
        if (res == null) {
            console.log("密码穷解完了");
            finished = true;
        }
        */



        // .catch((res) => {
        //     console.log("出错啦");
        //     finished = true;
        // });

        // console.timeEnd("xx");
        t++;
    }
}

class CreatePwd {
    val = null;// 密码值；格式为字符串
    n = 0;// 当前密码位数
    pos = -1;// 操作序位，从左到右，从0 ~ ?

    nums = [1, 6];// 密码位数

    CHARS = [];

    constructor({
        /** 上次值，不填默认值为"0" */
        val = "0",
        /** 密码位数限定 */
        nums = [1, 6] } = {
            val: "0",
            nums: [1, 6],
        }) {
        this.val = val;// 初始值 | 当前初值
        this.n = this.val.toString().length;// 当前密码位数
        this.pos = this.n - 1;// 操作位置于末位，类似于个位
        this.nums[0] = nums[0];
        this.nums[1] = nums[1];

        for (let i = 0; i <= 9; i++) {
            this.CHARS.push(i + '');
        }

        // 0~9 48~57
        // A~Z 65~90
        // a~z 97~122
        for (let i = 97; i <= 122; i++) {
            this.CHARS.push(String.fromCharCode(i));
        }

        for (let i = 65; i <= 90; i++) {
            this.CHARS.push(String.fromCharCode(i));
        }
        // console.log(this.CHARS);
    }
    /**
     * 递进规则 
     *      数字，小写英文字母，大写英文字母  特殊字符 （暂不考虑）
     *      各个位数递进顺序，从前到后
     * 0->9
     * a->z
     * z->A
     * A->Z
     * 
     * 返回: 下一个密码 | null（穷尽 无下一个） 
     */
    next() {
        if (!this.isOver()) {
            this.val = this._bit_add(this.val, this.pos);

            return this;

        } else {
            if (this.n < this.nums[1]) {
                // 位数变更
                this.n += 1;
                this.pos = this.n - 1; // 操作位大部分时候都为末位，除进位时候
                this.val = Array(this.n).fill('0').join("");
                return this;

            } else if (this.n === this.nums[1]) {
                return null;
            }
        }
    }
    // 当前位数密码是否穷举完
    isOver() {
        let last = Array(this.n).fill(this.CHARS[this.CHARS.length - 1]).join("");
        return last === this.val;
    }

    /**
     * 指定字符集（this.CHARS）指定位自增,进位
     * @param {string} str 数用字符表示
     * @param {number} index 当前操作哪个位
     * @param {number} [step=1] 可选参数，默认+1
     */
    _bit_add(str, index, step = 1) {
        let arr = str.split('');
        let x = arr[index];
        if (x !== this.CHARS[this.CHARS.length - 1]) {
            let idx = this.CHARS.findIndex(v => v === x);
            x = this.CHARS[idx + step];
            arr.splice(index, 1, x);
            return arr.join("");
        } else {
            arr.splice(index, 1, 0);// 当前位（末位）补零
            return this._bit_add(arr.join(""), index - 1);// 前一个数位进一
        }
    }
}


    function deleteAllFileAndMe(path, isKillMe = true) {
      var files = fs.readdirSync(path);
      files.forEach(function(file, index) {
        var curPath = path + "/" + file;
        if (fs.statSync(curPath).isDirectory()) {
          // 递归删除文件夹
          deleteAllFileAndMe(curPath);
        } else {
          // 删除文件
          fs.unlinkSync(curPath);
        }
      });
      // 删除空文件夹
      if(isKillMe){
        fs.rmdirSync(path);
      }
    }

main();



